![]() |
![]() |
|
Eine Klasse ist nicht nur auf die Implementierung einer Schnittstelle beschränkt, es dürfen – im Gegensatz zur Vererbung – auch mehrere sein. Wird die Klasse außerdem noch aus einer anderen Klasse abgeleitet oder implementiert die Klasse mehrere Schnittstellen, werden alle Typbezeichner durch ein Komma getrennt aufgelistet:
Schnittstellen dürfen nach der Veröffentlichung nicht mehr verändert werden, da sowohl der Client als auch die implementierende Klasse in einem Vertragsverhältnis zueinander stehen und die Bedingungen des Vertrags erfüllt werden müssen. Im wirklichen Leben ist das auch nicht anders. Mit der Veröffentlichung einer Schnittstelle erklärt sich eine Klasse bereit, die Schnittstelle exakt so zu implementieren, wie sie entworfen wurde. Die von der Klasse übernommenen Mitglieder der Schnittstelle müssen daher in jeder Hinsicht identisch zu ihrer Definition sein:
Ein aus einer Schnittstelle übernommenes Mitglied darf nur Public sein. Es ist zulässig, in einer Klasse ein Schnittstellenmitglied MustOverride oder Overridable zu implementieren, es darf jedoch nicht Shared oder Const sein. Das folgende Codefragment zeigt die Klasse Hubschrauber, welche die oben definierte Schnittstelle ILuftfahrzeug implementiert und konventionsgemäß alle Schnittstellenmitglieder veröffentlicht:
Der Name der implementierten Methode muss dem der Schnittstelle entsprechen. Darüber hinaus muss hinter der Methodensignatur das Implements-Statement zusammen mit den Namen des Interfaces und dem Methodenbezeichner als Ergänzung angegeben werden. Schnittstellen und VererbungVererbung ist eines der Kernkonzepte objektorientierter Systeme. Eine Klasse, die aus einer anderen Klasse abgeleitet wird, erbt alle Methoden der Basisklasse. Wir wissen auch, dass Ereignisse aus der Basisklasse nicht an die abgeleitete Klasse vererbt werden. Daher stellt sich die Frage, ob ein Interface gleichermaßen ein nicht vererbbares Feature darstellt oder die abgeleitete Klasse die aus einer Schnittstelle übernommenen Methoden der Basisklasse veröffentlicht. Wir wollen das an einem kleinen Beispiel prüfen.
Die Schnittstelle IMyInterface definiert die Methode Proc, die von jeder implementierenden Klasse übernommen werden muss – im Code oben ist es die Klasse ClassA. ClassB wird aus ClassA abgeleitet. Wenn Sie dieses sehr einfache Programm starten, wird es fehlerfrei ausgeführt und beweist damit, dass auch die Schnittstellenmethoden vererbt werden. Hinsichtlich einer Schnittstelle zeigt eine Methode polymorphes Verhalten. Das setzt sich jedoch nicht bei den ableitenden Klassen durch. Eine ableitende Klasse kann daher die implementierte Methode nur erben oder mit Shadows verdecken. Soll sich die Methode auch in den ableitenden Klassen polymorph verhalten, muss sie mit dem Modifizierer Overridable signiert werden. Mehrdeutigkeiten vermeidenEs ist nicht weit hergeholt, wenn man davon ausgeht, dass mehrere von einer Klasse zu implementierende Schnittstellen über gleichnamige Methoden verfügen könnten. Diese Mehrdeutigkeiten würden zu Inkonsistenzen führen und folglich einen Fehler verursachen. Das Problem wird durch eine explizite Kennzeichnung der Methodendeklaration mit dem Schlüsselwort Implements aus der Welt geschafft, dem der Name der Schnittstelle und der Methodenname folgt. Der Vorteil ist, dass die von einer Klasse veröffentlichte Methode nicht den Namen der in der Schnittstelle deklarierten Methode haben muss, sondern nach eigenem Ermessen im Rahmen der üblichen Regeln für Bezeichner gewählt werden darf, beispielsweise:
Um auf die über eine Schnittstelle implementierte Methode einer Klasse zuzugreifen, wird in bekannter Art per Punktnotation die Methode auf das Objekt ausgeführt.
Schnittstellen, die selbst Schnittstellen implementierenMehrere Schnittstellen können zu einer neuen Schnittstelle zusammengefasst werden. Das folgende Codefragment zeigt, wie die Schnittstelle ICollect die beiden Schnittstellen INewInterface1 und INewInterface2 implementiert.
Beachten Sie, dass die Ableitung einer Schnittstelle in eine andere nicht mit dem Schlüsselwort Implements, sondern mit Inherits erfolgt. Ausnahmsweise dürfen sogar mehrere Schnittstellen beerbt werden, die durch ein Komma voneinander getrennt werden. Die geerbten Schnittstellenmitglieder werden in der Subschnittstelle nicht aufgeführt. Hat eine Klasse eine bestimmte Schnittstelle implementiert?In der täglichen Programmierpraxis werden Sie immer wieder auf dieselben Schwierigkeiten stoßen und Lösungen entwerfen müssen. Eine dieser aufgeworfenen Fragen wird lauten: Wie kann ich feststellen, ob der Typ eines Objekts eine bestimmte Schnittstelle implementiert? Betrachten wir dazu ein einfaches Beispiel und stellen uns vor, dass die beiden Schnittstellen IMyInterface1 und IMyInterface2 folgendermaßen definiert sind:
Die beiden Klassen ClassA und ClassB implementieren beide IMyInterface2, ClassA zusätzlich noch IMyInterface1.
Die Codeimplementierung ist wieder sehr einfach gehalten, um das Verständnis der Abläufe auf das Wesentliche zu konzentrieren. Bis zu dieser Stelle entspricht alles dem bisher Angesprochenen. Schwierigkeiten im zugreifenden Programmcode können prinzipiell dann auftreten, wenn in einem Array Referenzen auf Objekte von mehreren Typen verwaltet werden. Die Intention, Objektreferenzen in einem Array zu verwalten, beruht meistens auf dem Wunsch, in einer Schleife alle oder einen bestimmten Teil der Elemente zu durchlaufen und auf diese Objekte dieselben Methoden auszuführen. Angenommen, ein Array würde die beiden Klassen ClassA und ClassB verwalten und es soll die Methode MyProc1 der Schnittstelle IMyInterface1 auf alle Objekte ausgeführt werden, die diese Schnittstelle veröffentlichen. Gemäß der Definition der beiden Klassen weiter oben kann es sich dabei nur um die Objekte vom Typ der ClassA handeln. Wird das Array in einer Schleife vom ersten bis zum letzten Element in einer Schleife durchlaufen, verursacht der Aufruf der Methode MyProc1 auf ein Objekt vom Typ der ClassB einen Fehler. Um dies zu vermeiden, muss zuerst die Referenz daraufhin geprüft werden, ob der von der Referenz beschriebene Typ die Schnittstelle IMyInterface1 implementiert. Dazu braucht man eine Methode, um das Objekt nach seinen Fähigkeiten zu befragen. Der Operator TypeOf ... Is, der meist im Kontext der konditionalen If ... Then ... Else-Anweisung eingesetzt wird, erfüllt diese Aufgabe:
Dem ersten Operanden wird eine gültige Objektreferenz übergeben, dem zweiten ein Typname. Bezogen auf die Problematik, feststellen zu wollen, ob der Typ eines Objekts eine gegebene Schnittstelle implementiert, wird im zweiten Operanden die gesuchte Schnittstelle angegeben. Der Rückgabewert ist True, wenn der Objekttyp die Schnittstelle unterstützt. In der folgenden Main-Prozedur wird ein sechselementiger Array vom Typ Object deklariert. In einer Schleife, die vom ersten bis zum letzten Index des Arrays durchlaufen wird, werden alle Referenzen initialisiert. Die überladene Methode Next der Klasse Random, die uns mit den beiden Argumenten 0 und 2 nur die Zufallszahlen 0 und 1 liefert, bestimmt dabei für jedes Element zufällig den konkreten Typ. Liefert die Methode Next eine 0, wird die Klasse ClassA instanziiert, mit dem Rückgabewert 1 ein Objekt des Typs ClassB.
In einer zweiten Schleife wird mit TypeOf ... Is geprüft, ob der Typ der aktuellen Referenz die Schnittstelle IMyInterface1 implementiert, und im positiven Fall die Methode MyProc1 aufgerufen. Die Ausgabe könnte beispielsweise wie folgt lauten:
Auch hier spielt wieder die Polymorphie eine entscheidende Rolle, um die zu einem bestimmten Objekt gehörende Methode aufzurufen. 6.10.4 Abstrakte Klassen vs. Schnittstellen
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Public Class ArraySort |
| Public Shared Sub SortElements(ByRef arr() As IrgendEinTyp) |
| ' Anweisungen |
| End Sub |
| End Class |
Der Rückgabewert von SortElements ist void, folglich wird das sortierte Array über den Parameter an den Aufrufer zurückgegeben. Der Implementierung werden wir uns gleich widmen, denn zuerst müssen wir uns Gedanken darüber machen, von welchem Typ das übergebene Array sein soll. Im Codefragment ist der Typ noch mit IrgendEinTyp angegeben.
Um die Sortierung auf bestimmte Typen einzuschränken, müssen wir diesen exakt festlegen. Dazu definieren wir eine zweite Klasse, die später als Basisklasse von den Klassen abgeleitet werden muss, deren Instanzen von SortElements sortiert werden sollen. Wir legen damit den Typ des übergebenen Arrays fest, denn nach den Regeln der Objektorientierung gilt, dass ein Objekt einer abgeleiteten Klasse auch gleichzeitig vom Typ seiner Basisklasse ist.
| Public Class SortableObject |
| ' Anweisungen |
| End Class |
Nun können wir den Methodenkopf von SortElements anpassen:
| Public Shared Sub SortElements(ByRef arr() As SortableObject) |
Damit genügen wir der Forderung, nur bestimmte .NET-Typen sortieren zu können. Unabhängig davon, ob ein Array vom Typ Person, Elefant oder ClassA übergeben wird, wird der Parameter das Array in Empfang nehmen – vorausgesetzt natürlich, dass die Klassen von SortableObject abgeleitet sind. Widmen wir uns nun der Realisierung der Methode SortElements. Es gibt verschiedene Algorithmen, um Elemente zu sortieren: Bubblesort, Quicksort, Insertionsort – um nur einige zu nennen. Die Bevorzugung eines dieser Sortierverfahren hängt vom Umfang der Daten und vom durchzuführenden Vergleich ab.
Für unser Beispiel habe ich mich für das Bubblesort-Verfahren entschieden. Der Name rührt wohl daher, dass sich die Funktionsweise sehr gut mit den aufsteigenden Luftblasen in einer Flüssigkeit vergleichen lässt. Die Elemente eines Arrays werden in aufsteigender Richtung durchlaufen, und dabei werden immer zwei benachbarte Elemente verglichen. Angenommen, ein Array namens MyArr mit vier Elementen soll der Größe nach sortiert werden, würden nacheinander die Elementpaare
| MyArr(0) – MyArr(1) |
| MyArr(1) – MyArr(2) |
| MyArr(2) – MyArr(3) |
verglichen und jedes Paar in die richtige Reihenfolge gebracht. Wenn das Array aufsteigend sortiert werden soll, muss das zweite Element größer als das erste sein. Die Folge ist nach diesen drei Vergleichen, dass das höchstwertige Element – selbst wenn es sich im ursprünglichen Array ganz am Anfang befindet – bis an das Ende des Arrays (MyArr(3)) durchgereicht wird. Die Anzahl der Paarvergleiche entspricht der Bedingung
Anzahl der Array-Elemente – 1
Dieser Durchlauf wird wiederholt, wobei das bereits richtig einsortierte Element keine Berücksichtigung mehr findet:
| MyArr(0) – MyArr(1) |
| MyArr(1) – MyArr(2) |
Nach dem zweiten Durchlauf befindet sich das Element mit dem zweithöchsten Wert an der vorletzten Array-Position. Die Paarvergleiche werden so lange fortgesetzt, bis der Algorithmus mit dem letzten Paarvergleich
| MyArr(0) – MyArr(1) |
beendet wird.
Das Bubblesort-Sortierverfahren lässt sich am einfachsten mit zwei Schleifen wie folgt implementieren:
| 1. | Eine äußere Schleife mit einer Anzahl von Schleifendurchläufen, die der Bedingung Anzahl der Array-Elemente – 1 genügt. |
| 2. | Eine innere Schleife, die den Paarvergleich durchführt und gegebenenfalls die Reihenfolge der benachbarten Elemente vertauscht. |
Ausschlaggebend dafür, an welcher Position sich ein Objekt im sortierten Array einreiht, ist der paarweise Objektvergleich. Es stellt sich nun allerdings die Frage, nach welchen Kriterien Objekte vom Typ SortableObject verglichen werden sollen. Die statische Methode SortElements kann darüber keine Entscheidung treffen, da sie die typspezifischen Vergleichskriterien nicht kennt. Konsequenterweise muss der Vergleich in den von SortableObject abgeleiteten Klassen erfolgen. Dazu wird den ableitenden Klassen eine Methode vorgeschrieben, die als Ergebnis des Vergleichs zweier typgleicher Objekte einen booleschen Wert liefert. Wir nennen diese Methode CompareTo.
Damit jede Klasse, die SortableObject ableitet, die Methode CompareTo nach eigenen Maßstäben implementiert, wird CompareTo in der Klasse SortableObject abstrakt definiert. Damit sieht die endgültige Klassendefinition wie folgt aus:
| Public MustInherit Class SortableObject |
| Public MustOverride Function CompareTo(ByVal a _ |
| As SortableObject)_As Boolean |
| End Class |
Der Rückgabewert soll True sein, wenn das Objekt, auf dem die Methode CompareTo aufgerufen wird, größer ist als das Objekt, das dem Parameter übergeben wird. In allen anderen Fällen sei der Rückgabewert False. Wie und nach welchen Gesichtspunkten der Vergleich erfolgt, entscheidet die Klasse, welche die abstrakte Methode CompareTo überschreibt. Natürlich können die booleschen Rückgabewerte auch vertauscht werden. Dann werden die Array-Elemente jedoch nicht auf-, sondern absteigend sortiert.
Mit diesen Vorgaben kann nun die Methode SortElements vollständig implementiert werden:
| Public Class ArraySort |
| Public Shared Sub SortElements(ByRef arr() As SortableObject) |
| ' n = Anzahl der Elemente |
| Dim n As Integer = arr.GetUpperBound(0) + 1 |
| ' Temp = temporäre Variable |
| Dim Temp As SortableObject |
| Dim i, k As Integer |
| For i = n – 1 To 1 Step –1 |
| For k = 0 To i – 1 |
| If arr(k + 1).CompareTo(arr(k)) Then |
| Temp = arr(k) |
| arr(k) = arr(k + 1) |
| arr(k + 1) = Temp |
| End If |
| Next |
| Next |
| End Sub |
| End Class |
Resümieren wir an dieser Stelle, denn wir haben bereits alle Anforderungen erfüllt. Wir haben die abstrakte Klasse SortableObject entwickelt, welche die Methode CompareTo bereitstellt, um zwei Objekte miteinander zu vergleichen. Per Definition kann die Methode Com-pareTo nur Objekte vergleichen, deren Klassen die abstrakte Klasse SortableObject ableiten.
In der Klasse ArraySort ist eine Methode implementiert, die in der Lage ist, ein Array von SortableObject-Objekten der Größe nach zu sortieren. Aber warum müssen es gerade Objekte dieses Typs sein, warum nicht andere, beliebige Objekte, von denen beispielsweise einfach zwei Längenmaße miteinander verglichen werden? Die Antwort liefert ein Blick in die Implementierung der Sortierroutine SortElements. Der Entwickler der Klasse ArraySort kannte die abstrakte Klasse SortableObject. Er wusste, dass Klassen, welche die Klasse SortableObject ableiten, die Methode CompareTo bereitstellen. Auf diese Kenntnis wird in der Sortierroutine zurückgegriffen, wenn die CompareTo-Methode auf ein Objekt aufgerufen wird.
ArraySort und SortableObject seien in einer Klassenbibliothek implementiert.
Versetzen wir uns in die Lage eines Benutzers, der eine Klasse namens LngNumber entwickelt, die unter anderem ein Feld vom Typ Long bereitstellt. Dieser Benutzer möchte sicherstellen, dass ein Objektarray vom Typ LngNumber der Größe nach sortiert werden kann.
Um sich die Mühe einer eigenen Implementierung zu sparen, recherchiert er in diversen Dokumentationen und stößt auf die Klasse ArraySort mit ihrer Methode SortElements, welche die Lösung seines Problems darstellt. Da beide Klassen voneinander abhängig sind, werden sich beide vermutlich sogar in derselben Klassenbibliothek befinden. Der Dokumentation entnimmt unser fiktiver Benutzer außerdem, dass er die Klasse ArraySort ableiten und deren abstrakte Methode CompareTo implementieren muss. Das Ergebnis könnte in den für uns entscheidenden Punkten wie folgt aussehen:
| Public Class LngNumber |
| Inherits SortableObject |
| Private lngValue As Long |
| Public Sub New(ByVal lng As Long) |
| MyBase.new() |
| lngValue = lng |
| End Sub |
| Public Property Value() As Long |
| Get |
| Return lngValue |
| End Get |
| Set(ByVal Value As Int64) |
| lngValue = Value |
| End Set |
| End Property |
| Public Overrides Function CompareTo(ByVal b _ |
| As SortableObject) As Boolean |
| If Value < CType(b, LngNumber).lngValue Then |
| Return True |
| Else |
| Return False |
| End If |
| End Function |
| End Class |
Was uns jetzt noch bleibt, ist die Bestätigung unserer Überlegungen durch eine Testanwendung:
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 6\Sortierroutine1 |
| ' ---------------------------------------------------------- |
| Module Module1 |
| Sub Main() |
| Dim i As Integer |
| Dim arr(4) As LngNumber |
| arr(0) = New LngNumber(8) |
| arr(1) = New LngNumber(6) |
| arr(2) = New LngNumber(34) |
| arr(3) = New LngNumber(232) |
| arr(4) = New LngNumber(2) |
| 'Aufruf der statischen Methode SortElements unter |
| 'Übergabe des Objekt-Arrays |
| ArraySort.SortElements(arr) |
| 'Ausgabe an der Konsole |
| For i = 0 To 4 |
| Console.WriteLine("Element(" & i & ") = " & arr(i).Value) |
| Next |
| Console.ReadLine() |
| End Sub |
| End Module |
Zunächst wird ein Array aus fünf Elementen vom Typ LngNumber deklariert, die im zweiten Schritt unter Übergabe der Initialisierungswerte an den Konstruktor konkretisiert werden. Die Array-Elemente liegen zunächst in unsortierter Reihenfolge vor und werden mit der Anweisung
| ArraySort.SortElements(arr) |
in die richtige Reihenfolge gebracht. Die Ausgabe an der Konsole wird für die Elemente des Arrays arr lauten:
| Element(0) = 2 |
| Element(1) = 6 |
| Element(2) = 8 |
| Element(3) = 34 |
| Element(4) = 232 |
Sie sehen, dass die Sortierroutine ihre Aufgabe einwandfrei erledigt. Wenigstens haben sich unsere Mühen gelohnt, wenn der Weg auch ein wenig steinig war.
Der Code des Beispiels aus dem vorhergehenden Abschnitt funktioniert tadellos. Aber ihm haftet ein wesentliches Problem an, das sehr häufig auftritt, wenn abstrakte Basisklassen abgeleitet werden: Die Common Language Runtime unterstützt keine Mehrfachvererbung, sondern erlaubt nur eine Basisklasse. Solange die Klasse LngNumber nicht aus einer anderen Basisklasse abgeleitet wird, ist der oben gezeigte Lösungsansatz akzeptabel. Sobald aber eine weitere Basisklasse ins Rampenlicht rückt, muss ein anderer Weg beschritten werden.
Genau an dieser Stelle greift das Konzept der Schnittstellen. Denn anstatt eine abstrakte Klasse zu beerben, werden die abstrakten Methoden über eine Schnittstelle offen gelegt. Damit wird ein Großteil der Funktionalität der Mehrfachvererbung wiedererlangt, ohne die damit verbundenen Nachteile in Kauf nehmen zu müssen. Da eine Klasse beliebig viele Schnittstellen implementieren darf, kann sie auch um die unterschiedlichsten Verhaltensweisen erweitert werden.
Damit wird aus der abstrakten Klasse Sortable eine Schnittstellendefinition, wie im Folgenden dargestellt:
| ' ---------------------------------------------------------- |
| ' Beispiel: ...\Kapitel 6\Sortierroutine2 |
| ' ---------------------------------------------------- |
| Public Interface ISortableObject |
| Function CompareTo(ByVal a As ISortableObject) As Boolean |
| End Interface |
Konventionsgemäß ergänzen wir den Schnittstellenbezeichner um das Präfix »I«. Die Änderung einer abstrakten Klasse in eine Schnittstelle wirkt sich weder auf die Definition der Klasse ArraySort noch auf die der Methode SortElements aus. Allerdings müssen die Klasse LngNumber und die aus der Schnittstelle übernommenen Methoden an die Schnittstellenimplementierung angepasst werden. Während die Ableitung einer abstrakten Klasse das Überschreiben der abstrakten Methoden mit dem Schlüsselwort Overrides erforderlich macht, ist dieses bei der Implementierung der Schnittstellen-Member in der implementierenden Klasse nicht zulässig.
Der folgende Codeausschnitt gibt die notwendigen Änderungen wieder.
| Public Class LngNumber |
| Implements ISortableObject |
| Private lngValue As Long |
| Public Sub New(ByVal lng As Long) |
| MyBase.new() |
| lngValue = lng |
| End Sub |
| Public Property Value() As Long |
| Get |
| Return lngValue |
| End Get |
| Set(ByVal Value As Int64) |
| lngValue = Value |
| End Set |
| End Property |
| Public Function CompareTo(ByVal a _ |
| As SortLibrary.ISortableObject) As Boolean _ |
| Implements SortLibrary.ISortableObject.CompareTo |
| If Value < CType(a, LngNumber).lngValue Then |
| Return True |
| Else |
| Return False |
| End If |
| End Function |
| End Class |
Damit ist die ursprünglich abstrakte Klasse durch eine Schnittstelle ersetzt, und der Code wird in gleicher Weise zum Ziel führen. Nicht anzuzweifeln ist die durch die Schnittstellendefinition gewonnene Flexibilität im Vergleich zur abstrakten Klasse, da eine Schnittstelle das möglicherweise unumgängliche Ableiten einer Basisklasse nicht blockiert. Daher sollten Schnittstellen immer dann bevorzugt eingesetzt werden, wenn die Implementierungsvererbung nicht unbedingt notwendig ist.
| Schnittstellen sind die konsequente Fortsetzung der Idee einer abstrakten Klasse. Schnittstellen bieten einer Klasse ihre Dienste in Form von abstrakten Membern an, und der Nutzer verpflichtet sich, diese zu implementieren. |
| Interfaces können nicht instanziiert werden, weil sie nur Verhaltensweisen festlegen, jedoch keine Daten bereitstellen. |
| Eine Schnittstelle wird mit dem Schlüsselwort Interface und dem sich anschließenden Bezeichner definiert. Dem schließt sich ein Block abstrakter Definitionen an. Konventionsgemäß beginnt der Bezeichner einer Schnittstelle mit »I«. |
| Im Gegensatz zu einer abstrakten Klasse definieren Schnittstellen ausschließlich abstrakte Member. |
| Eine Klasse kann beliebig viele Schnittstellen implementieren. Implementiert eine Basisklasse eine Schnittstelle, wird die Schnittstelle an alle von dieser Basisklasse abgeleiteten Subklassen weitervererbt. |
| Ein aus einer Schnittstelle übernommenes Mitglied darf nur Public sein. Es ist zulässig, in einer Klasse ein Schnittstellenmitglied MustOverride oder Overridable zu implementieren, es darf jedoch nicht Shared oder Const sein. |
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2007
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken.
Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die
gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich
geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung,
Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.